package cn.edu.dgut.css.sai.dgutapp;

import hello.HelloService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.SpringBootExceptionReporter;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.event.EventPublishingRunListener;
import org.springframework.boot.diagnostics.*;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.boot.env.RandomValuePropertySource;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.*;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.env.*;
import org.springframework.core.io.support.ResourcePropertySource;
import org.springframework.stereotype.Component;

/**
 * 企业级开发框架实践课程 - Spring Boot自动配置原理及源码分析。
 *
 * @author sai
 * @see SpringBootConfiguration
 * @see ComponentScan
 * @see ObjectProvider
 * @since 2020/10/7
 */
@SpringBootApplication
public class DgutApp implements InitializingBean {

    /**
     * 在{@link Configuration}配置类上使用{@link PropertySource}注解可以添加额外的属性源。
     * <p>属性源类型为{@link ResourcePropertySource}。</p>
     * <p>
     * Spring Boot在启动过程中，会创建多个属性源，并封装到{@link PropertySourcesPropertyResolver},
     * 所以查找读取属性源的属性键值时，都是由{@code PropertySourcesPropertyResolver}去处理。
     * </p>
     * <p>在{@code @PropertySource}中可以设置使用什么字符编码读取文件，设置为utf-8时可以解决中文乱码的问题。</p>
     * <p>源码分析:</p>
     * <p>{@link PropertySource}注解是在{@code ConfigurationClassParser#processPropertySource}解释配置类的时候处理的。</p>
     * <p>在spring boot启动过程中，会使用{@link ComponentScan}扫描组件{@link Component},如果组件类上有{@link Component}、{@link ComponentScan}、{@link Import}、{@link ImportResource}，则spring boot会认为这个组件是个配置类，会按解释配置类的步骤处理它。</p>
     * <p>了解源码后，尝试把下面的{@code @Configuration}注解改为{@code @Component}，检查是否还能读取到配置文件的属性值。</p>
     *
     * @see PropertySourcesPropertyResolver
     * @see PropertiesPropertySource
     * @see CompositePropertySource
     * @see ResourcePropertySource
     * @see OriginTrackedMapPropertySource
     * @see RandomValuePropertySource
     * @see CommandLinePropertySource
     */
    @Configuration
    @PropertySource(value = "classpath:config/settings", encoding = "UTF-8")
    static class PropertyConfiguration implements InitializingBean, EnvironmentAware {

        Environment env;

        @Override
        public void afterPropertiesSet() {
            System.out.printf("dgut.settings 的值为: %s%n", env.getProperty("dgut.settings"));
        }

        @Override
        public void setEnvironment(Environment environment) {
            this.env = environment;
        }
    }


    public static void main(String[] args) {

        new SpringApplicationBuilder()
                .sources(DgutApp.class)
                .listeners(new HelloApplicationListener())
                .run(args);
    }

    @Bean
    CommandLineRunner init(ObjectProvider<HelloService> helloServiceProvider) {
        return args -> helloServiceProvider.ifAvailable(helloService -> helloService.sayHello("黎志雄"));
    }

    /**
     * 自定义事件监听器
     * <p></p>
     * 向spring容器的事件发布器{@link ApplicationEventMulticaster}注册事件监听器的方式：
     * <ol>
     * <li>可以从spring.factories文件注入事件监听器；</li>
     * <li>通过{@link SpringApplicationBuilder}添加事件监听器；</li>
     * <li>把事件监听器注册为Bean(@Component、@Bean)。</li>
     * </ol>
     * <p>
     * {@code SpringApplication#prepareContext} spring boot启动时到了prepareContext，会利用{@link EventPublishingRunListener#contextLoaded}把现有事件监听器注册到spring容器。
     * </p>
     * <p>
     * 另外，当spring容器刷新时refresh方法执行到prepareBeanFactory方法，里面会注册一个Bean的后处理器{@code ApplicationListenerDetector}，
     * 这个后处理器很重要，顾名思义，它会在所有Bean在实例化后，检查这个Bean是不是 事件监听器类型，如果 是，则把它 注册到 spring 容器中。
     * </p>
     *
     * @see ApplicationContextInitializedEvent
     * @see ApplicationPreparedEvent
     * @see ApplicationEventMulticaster
     * @see AbstractApplicationContext#addApplicationListener(ApplicationListener)
     */
//    @Component
    static class HelloApplicationListener implements ApplicationListener<ApplicationEvent> {

        @Override
        public void onApplicationEvent(ApplicationEvent event) {
            System.out.println("事件：" + event.getClass().getName());
        }

    }

    /**
     * 使用{@link Value}注解可以获取配置文件的dgut.name属性值,并注入Bean的成员属性中。<br/>
     * 但如果配置文件中没有声明dgut.name属性时，spring boot启动时会抛{@link IllegalArgumentException}。
     */
    @Value("${dgut.name}")
    private String dgutName;

    /**
     * 在{@link Value}注解中使用SpEl表达式，调用bean的方法，并把方法返回值注入到bean的成员属性中。
     */
    @Value("#{environment.getProperty('random.')}")
    private String RandomStr;

    @Override
    public void afterPropertiesSet() {
        System.out.println("dgut.name = " + dgutName);
        System.out.println("RandomStr = " + RandomStr);
    }

    /**
     * 自定义{@link FailureAnalyzer}
     * <p></p>
     * <p>
     * Spring Boot在项目启动时，如果遇到异常并不能友好的打印出具体的堆栈错误信息，我们只能查看到简单的错误消息，以致于并不能及时解决发生的问题，
     * 针对这个问题SpringBoot提供了故障分析仪的概念（failure-analyzer），spring boot内部根据不同类型的异常提供了一些FailureAnalyzer实现。
     * Spring Boot提供了扩展点，我们可以自定义针对特定异常类型的{@link FailureAnalyzer}，然后在spring.factories文件中部署它，Key为org.springframework.boot.diagnostics.FailureAnalyzer，
     * 这样spring boot在启动流程中会会通过spring.factories的加载机制加载它。
     * 当spring boot在启动过程中有异常发生，会捕获异常，并把它交给异常类型匹配的FailureAnalyzer去处理。
     * </p>
     * 从Spring Boot的启动源码分析可知，自定义FailureAnalyzer的步骤如下：<br/>
     * <ol>
     *     <li>实现在自定义的异常类；</li>
     *     <li>实现自定义的FailureAnalyzer类；</li>
     *     <li>在spring.factories文件中，配置一个key为org.springframework.boot.diagnostics.FailureAnalyzer的值为这个自定义FailureAnalyzer类的类名。</li>
     * </ol>
     * {@code FailureAnalyzers}
     *
     * @see SpringApplication#run(String...)
     * @see SpringBootExceptionReporter
     * @see FailureAnalysis
     * @see FailureAnalyzer
     * @see AbstractFailureAnalyzer
     * @see FailureAnalysisReporter
     * @see LoggingFailureAnalysisReporter
     */
    static class CustomFailureAnalyzer extends AbstractFailureAnalyzer<IllegalArgumentException> {

        @Override
        protected FailureAnalysis analyze(Throwable rootFailure, IllegalArgumentException cause) {
            return new FailureAnalysis(
                    String.format("dgut.name属性值获取异常 %s", cause.getMessage()),
                    "dgut.name 必须在配置文件中声明并赋值，可以留空值。", cause);
        }
    }

}
